Introduction
Dynamic programming is an algorithm design paradigm where the problems have optimal substructure and overlapping subproblems. They have specific optimization techniques to solve them faster than usual.
Fibonacci sequence is a good example of dynamic programming problem, we will use it throughout this chapter and several following chapters to explain the fundamental concepts of dynamic programming.
Let be the -th Fibonacci number, and it is defined as follows:
Which translate to the following recursive function:
function fibonacci(n: int) -> int:
// base cases
if n == 0: return 0
if n == 1: return 1
// recursive case
return fibonacci(n - 1) + fibonacci(n - 2)
The first few Fibonacci numbers are:
Optimal Substructure & Overlapping Subproblems
We will start by explaining the two properties of dynamic programming problems: optimal substructure and overlapping subproblems.
Similar to divide and conquer, dynamic programming problems have optimal substructure, but the difference is that in dynamic programming, the subproblems overlap. These two properties define the dynamic programming problems.
Optimal Substructure
A problem has an optimal substructure if optimal solution of its subproblems can be used to find the optimal solution of the whole problem.
In Fibonacci sequence, is computed by using and . The optimal solutions for and are necessary to compute the optimal solution of , so it has an optimal substructure.
Optimal solution is the solution which maximizes efficiency or minimizes cost, while correct solution is the solution which satisfy the constraint of the problem. However, we can use them interchangeably if the problem only has one solution or is already a maximization or minimization problem.
For example, if we want to compute , we need to have the optimal solution for and first, which are and , then we can compute correctly.
If instead we have the wrong values for a subproblem, let's say the under is miscalculated as , then we will obtain the wrong value and leading to the wrong value .
Optimal substructure shown in . Wrong value for leading to wrong value for .
Overlapping Subproblems
A problem has overlapping subproblems if its subproblems are reused multiple times throughout the computation of the whole problem.
In Fibonacci sequence, subproblems are reused multiple times. For example, if we expand , we can see that is reused three times. Since , so we performed this addition three times in .
While carrying out a simple addition may not be a big deal, when the problem size becomes larger, the overhead of repeating the same computation will become significant. For example, the computation will repeat 4181 times.
Overlapping subproblems shown in . Same subproblems are colored in the same color. Many subproblems are repeated.
Memoization & Bottom-up Approach
Memoization and bottom-up approach are two common optimization techniques for dynamic programming problems.
While techniques used to solve divide and conquer problems can be applied to dynamic programming problems, they are not as efficient as memoization and bottom-up approach due to the overlapping subproblems.
We will explore how these two techniques optimize the computation of overlapping subproblems.
Memoization
In dynamic programming, memoization is a technique to store the results of the overlapping subproblems.
This makes the number of times the same subproblem need to be calculated only once.
In Fibonacci sequence, before computing anything, we can check if we have already stored the result of the subproblem. If we have, we can just return the result without computing it again.
Bottom-up Approach
In dynamic programming, bottom-up approach is a technique to start solving the problem from the base cases. It can also memoize the results of the subproblems as we go up the recursion tree.
On top of that, it can avoid the overhead of recursion, as function calls are more expensive than results stored in plain variables.
In Fibonacci sequence, we can start from the base cases and , then compute the next Fibonacci number by using the previous two Fibonacci numbers. We can keep doing this until we reach the -th Fibonacci number.
Most of the time, we want to use the bottom-up approach to solve dynamic programming problems.
Solutions
Here are some notes about the solutions in this topic.
Pseudocode Syntax
We will use the following type annotations for pseudocode:
int
: integerfloat
: floating point number (or any number, really)bool
: booleanstr
: string(<type1>, <type2>, ..., <typen>)
: tuple of<type1>
,<type2>
, ...,<typen>
(<name1>: <type1>, <name2>: <type2>, ..., <namen>: <typen>)
: named tuple<type>[]
: array or list of<type>
Implementations
In this topic, we will provide pseudocode to the solution, and then language-specific implementations will be provided at the end of each chapter.
Currently, 3 languages are supported: Python, C++, and Rust.